package com.github.eclipseace.mybatis.entity.builder.xml;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.type.JdbcType;
import org.springframework.util.Assert;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.github.eclipseace.mybatis.entity.mapping.MappedEntity;
import com.github.eclipseace.mybatis.entity.mapping.PropertyMapping;
import com.github.eclipseace.mybatis.util.StringUtils;

/**
 * @author eclipseAce
 */
public class XmlTemplateRenderer {
	public static final String ROOT_ELEMENT_NAME = "template";

	private final MappedEntity entity;
	private final XNode rootNode;
	private final VariableContext rootContext = new VariableContext();
	private final Map<String, Node> partNodes = new HashMap<>();

	public XmlTemplateRenderer(MappedEntity entity, XPathParser parser) {
		this.entity = entity;
		this.rootNode = parser.evalNode("/" + ROOT_ELEMENT_NAME);

		for (XNode node : rootNode.evalNodes("part")) {
			String id = node.getStringAttribute("id");
			partNodes.put(id, node.getNode());
		}

		String prefix = this.rootNode.getStringAttribute("prefix");
		rootContext.put(prefix, "resultMapId", entity.getResultMapId());
		rootContext.put(prefix, "table", entity.getTable());
		rootContext.put(prefix, "catalog", entity.getCatalog());
		rootContext.put(prefix, "schema", entity.getSchema());
		PropertyMapping idProperty = entity.getIdProperty();
		rootContext.put(prefix, "id.name", idProperty.getName());
		rootContext.put(prefix, "id.column", idProperty.getColumn());
		rootContext.put(prefix, "id.jdbcType", idProperty.getJdbcType().toString());
	}

	protected List<Node> getChildNodeReferences(Node node) {
		Assert.isTrue(node.getNodeType() == Node.ELEMENT_NODE, "node is not element");
		List<Node> childNodeRefs = new ArrayList<>();
		NodeList childNodes = node.getChildNodes();
		for (int i = 0, len = childNodes.getLength(); i < len; ++i) {
			childNodeRefs.add(childNodes.item(i));
		}
		return childNodeRefs;
	}

	protected List<PropertyMapping> getFilteredProperties(String filter) {
		List<PropertyMapping> properties = new ArrayList<>();
		for (PropertyMapping property : entity.getAllProperties()) {
			if ("insertable".equals(filter) && !property.isInsertable()) {
				continue;
			}
			if ("updatable".equals(filter) && !property.isUpdatable()) {
				continue;
			}
			properties.add(property);
		}
		return properties;
	}

	protected void render(Node node, VariableContext context) {
		short nodeType = node.getNodeType();
		if (nodeType == Node.TEXT_NODE) {
			node.setTextContent(context.parseTokens(node.getTextContent()));
		} else if (nodeType == Node.ELEMENT_NODE) {
			NamedNodeMap attrNodes = node.getAttributes();
			for (int i = 0, len = attrNodes.getLength(); i < len; ++i) {
				Node attrNode = attrNodes.item(i);
				attrNode.setNodeValue(context.parseTokens(attrNode.getNodeValue()));
			}
			renderElement(node, context);
		}
	}

	protected void renderElement(Node node, VariableContext context) {
		String nodeName = node.getNodeName();
		if ("foreachProperty".equals(nodeName)) {
			renderForeachPropertyElement(node, context);
		} else if ("parameter".equals(nodeName)) {
			renderParameterElement(node, context);
		} else if ("includePart".equals(nodeName)) {
			renderIncludePartElement(node, context);
		} else {
			renderDefaultElement(node, context);
		}
	}

	protected void renderForeachPropertyElement(Node node, VariableContext context) {
		XNode xnode = rootNode.newXNode(node);
		String prefix = xnode.getStringAttribute("prefix");
		String separator = xnode.getStringAttribute("separator");
		String filter = xnode.getStringAttribute("filter");

		Document ownerDocument = node.getOwnerDocument();
		Node parentNode = node.getParentNode();
		List<Node> childNodes = getChildNodeReferences(node);
		Iterator<PropertyMapping> iterator = getFilteredProperties(filter).iterator();
		while (iterator.hasNext()) {
			PropertyMapping property = iterator.next();
			VariableContext newContext = context.newContext();
			newContext.put(prefix, "name", property.getName());
			newContext.put(prefix, "column", property.getColumn());
			newContext.put(prefix, "jdbcType", property.getJdbcType().toString());

			for (Node childNode : childNodes) {
				Node newChildNode = childNode.cloneNode(true);
				parentNode.insertBefore(newChildNode, node);
				render(newChildNode, newContext);
			}
			if (StringUtils.isNotEmpty(separator) && iterator.hasNext()) {
				parentNode.insertBefore(ownerDocument.createTextNode(separator), node);
			}
		}
		parentNode.removeChild(node);
	}

	protected void renderParameterElement(Node node, VariableContext context) {
		XNode xnode = rootNode.newXNode(node);
		String name = xnode.getStringAttribute("name");
		String jdbcType = xnode.getStringAttribute("jdbcType");

		StringBuilder script = new StringBuilder();
		script.append("#{").append(name);
		if (jdbcType != null && !JdbcType.UNDEFINED.toString().equals(jdbcType)) {
			script.append(",jdbcType=").append(jdbcType);
		}
		script.append("}");

		Document ownerDocument = node.getOwnerDocument();
		Node parentNode = node.getParentNode();
		parentNode.replaceChild(ownerDocument.createTextNode(script.toString()), node);
	}

	protected void renderIncludePartElement(Node node, VariableContext context) {
		XNode xnode = rootNode.newXNode(node);
		String ref = xnode.getStringAttribute("ref");

		Node refNode = partNodes.get(ref);
		if (refNode == null) {
			throw new IllegalStateException("Part element with id '" + ref + "' was not found");
		}

		VariableContext newContext = context.newContext();
		for (XNode variableNode : xnode.evalNodes("variable")) {
			String name = context.parseTokens(variableNode.getStringAttribute("name"));
			String value = context.parseTokens(variableNode.getStringAttribute("value"));
			newContext.put(name, value);
		}

		Node parentNode = node.getParentNode();
		for (Node partChildNode : getChildNodeReferences(refNode)) {
			Node newChildNode = partChildNode.cloneNode(true);
			parentNode.insertBefore(newChildNode, node);
			render(newChildNode, newContext);
		}
		parentNode.removeChild(node);
	}

	protected void renderDefaultElement(Node node, VariableContext context) {
		for (Node childNode : getChildNodeReferences(node)) {
			render(childNode, context);
		}
	}

	public List<XNode> render(String path) {
		List<XNode> nodes = new ArrayList<>();
		for (XNode originalNode : rootNode.evalNodes(path)) {
			Node rawNode = originalNode.getNode().cloneNode(true);
			render(rawNode, rootContext);
			nodes.add(rootNode.newXNode(rawNode));
		}
		return nodes;
	}
}
